/**
 * 
 */
package com.friendar.webapp;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Date;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.GregorianCalendar;

/**
 * This class is going to have a lot of math to compute the distance between 2
 * lat and longs For reference please see
 * http://www.movable-type.co.uk/scripts/latlong.html. Distance computation is
 * done using the Haversine formula which assumes the earth is spherical. the
 * accuracy if max of 3 m in 100m. This is a helper class used by other classes
 * in the package.
 * 
 * @author abansal
 * 
 */
public class FriendFinder {

	private static Connection conn = null;
	boolean connected;
	ArrayList<FriendInfoDTO> friends = new ArrayList<FriendInfoDTO>();

	public FriendFinder() {
		connected = false;
		try {
			if (conn == null) {
				Class.forName("com.mysql.jdbc.Driver").newInstance();

				conn = DriverManager
						.getConnection("jdbc:mysql://localhost/friendar?"
								+ "user=friendar&password=friendar");
				connected = true;
			}
		} catch (SQLException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (InstantiationException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IllegalAccessException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (ClassNotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

	}

	/**
	 * This method updates the distance to friends for a user ID in the friends
	 * table in the database
	 * 
	 * @param userId
	 * @return
	 */
	public boolean calcDistToFriends(String userId) {
		// First, retrieve list of location for all friends
		// empty the arraylist if it has members
		// TODO in the future, be intelligent about new locations / friends only
		friends.clear();
		double lat1 = 0, longi1 = 0;
		boolean retVal = false;
		try {
			// first get the location of the current user
			// if the location is more than a day old
			// then skip the entire processing
			PreparedStatement pst = conn
					.prepareStatement("select * from location where user_id=?");
			pst.setInt(1, Integer.valueOf(userId));
			ResultSet rs = pst.executeQuery();
			if (rs.first()) {
				lat1 = Double.valueOf(rs.getString("latitude"));
				longi1 = Double.valueOf(rs.getString("longitude"));
				Date dt = rs.getDate("time_update");
				Calendar c = new GregorianCalendar();
				c.setTimeInMillis(dt.getTime());
				Calendar no1 = new GregorianCalendar();
				//no1.setTimeInMillis(System.currentTimeMillis());

				//Date now = new Date(System.currentTimeMillis());// - 43200000);
				// // 12 hours
				// in the past
				no1.add(Calendar.HOUR, -12);

				/**if (c.before(no1)) {
					rs.close();
					pst.close();
					System.out.println("The user data not updated");
					System.out.println(c);
					System.out.println(no1);
					return retVal;
				}*/ //TODO fix the time comparison

			} else {
				System.out.println("No location data for user");
				rs.close();
				pst.close();
				return retVal;
			}
			// Statement st = conn.createStatement();
			pst = conn
					.prepareStatement("SELECT friend_1, friend_2, latitude, longitude, time_update "
							+ "FROM friends, location where friend_1 = ? and location.user_id = friends.friend_2");
			pst.setInt(1, Integer.valueOf(userId));
			rs = pst.executeQuery();
			// + " OR friend_2=" +userId);
			while (rs.next()) {
				int f1 = rs.getInt("friend_1");
				int f2 = rs.getInt("friend_2");

				FriendInfoDTO fd = new FriendInfoDTO();
				fd.userID = f1;
				fd.friendID = f2;
				fd.longi = rs.getString(4);
				fd.lat = rs.getString(3);
				fd.dtLoc = rs.getDate(5);
				fd.f1 = true;
				// get longi and lati
				friends.add(fd);
			}
			pst = conn
					.prepareStatement("SELECT friend_1, friend_2, latitude, longitude, time_update "
							+ "FROM friends, location where friend_2 = ? and location.user_id = friends.friend_1");
			pst.setInt(1, Integer.valueOf(userId));
			rs = pst.executeQuery();
			while (rs.next()) {
				int f1 = rs.getInt("friend_1");
				int f2 = rs.getInt("friend_2");
				FriendInfoDTO fd = new FriendInfoDTO();
				fd.userID = f2;
				fd.friendID = f1;
				// get longi and lati
				fd.longi = rs.getString(4);
				fd.lat = rs.getString(3);
				fd.dtLoc = rs.getDate(5);
				fd.f1 = false;
				friends.add(fd);
			}
			rs.close();
			pst.close();

		} catch (SQLException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
			return retVal;
		}
		// then compute distances and update the DB
		try {
			String update = "update friends set distance = ?, updated_on=sysdate()"
					+ "where friend_1 = ? and friend_2 = ?";
			PreparedStatement pst = conn.prepareStatement(update);

			for (int i = 0; i < friends.size(); i++) {
				FriendInfoDTO fd = friends.get(i);
				fd.distance = calcDistance(String.valueOf(lat1), String
						.valueOf(longi1), fd.lat, fd.longi);
				// then write the distance back to the database
				pst.setLong(1, fd.distance);
				if (fd.f1) {
					pst.setInt(2, fd.userID);
					pst.setInt(3, fd.friendID);
				} else {
					pst.setInt(3, fd.userID);
					pst.setInt(2, fd.friendID);
				}
				pst.addBatch(); // batch the updates to be completed as one
				// transaction
				System.out.println(fd);
			}
			int[] resp = pst.executeBatch();
			System.out.println("Result of update = " + resp);
			retVal = true;
			pst.close();
		} catch (SQLException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

		return retVal;
	}

	/**
	 * This method encapsulates all the math required to compute the distance
	 * between two lat and long co-ordinates. It is assumes the values are
	 * provided as signed decimals
	 * 
	 * @param lat1
	 * @param longi1
	 * @param lat2
	 * @param longi2
	 * @return distance in metres
	 */
	public long calcDistance(String lat1, String longi1, String lat2,
			String longi2) {

		double latit1 = Double.parseDouble(lat1);
		double latit2 = Double.parseDouble(lat2);
		double longit1 = Double.parseDouble(longi1);
		double longit2 = Double.parseDouble(longi2);

		double dLon = toRad((longit2 - longit1));
		double dLat = toRad(latit2 - latit1);

		double tmp1 = Math.sin(dLat / 2) * Math.sin(dLat / 2)
				+ Math.cos(toRad(latit1)) * Math.cos(toRad(latit2))
				* Math.sin(dLon / 2) * Math.sin(dLon / 2);

		double temp2 = 2 * Math.atan2(Math.sqrt(tmp1), Math.sqrt(1 - tmp1));

		double dist = 6371 * temp2; // 6371KM is mean radius of the earth

		double dist2 = 6371 * Math.acos(Math.sin(toRad(latit1))
				* Math.sin(toRad(latit2)) + Math.cos(toRad(latit1))
				* Math.cos(toRad(latit2)) * Math.cos(toRad(longit2 - longit1)));
		System.out
				.println("Difference in haversine and spherical law of cosines = "
						+ String.valueOf(dist - dist2));
		System.out.println(dist2);
		return (long) (dist * 1000); // as result is in metres
	}

	/**
	 * 
	 * @param userId
	 *            User for which firneds are to be found
	 * @param radii the radius within which friends must be in
	 * @return null if no friends found. Arraylist of FIDTO is found
	 */
	public ArrayList<FriendInfoDTO> getFriendDistances(String userId, long radii) {
		// SQL to get the friends and the distances
		// since this is a select and inner join, I expect that friends who do
		// not have distance in
		// the database will not be returned
		String sql = "SELECT friend_1 , friend_2 as friend, distance, updated_on, name, phone_num, email"
				+ " from friends f, users u"
				+ " where f.friend_2 = u.id"
				+ " and f.friend_1 = ? "
				+ "UNION"
				+ " SELECT  friend_2 , friend_1 as friend, distance, updated_on, name, phone_num, email"
				+ " from friends f, users u"
				+ " where f.friend_1 = u.id"
				+ " and f.friend_2 = ?;";

		// An arraylist of friendinfos which will be returned
		//ArrayList<FriendInfoDTO> friends = new ArrayList<FriendInfoDTO>();
		friends.clear();
		
		//if (connected) {
			try {
				// TODO add check for numericity of userID
				PreparedStatement pst = conn.prepareStatement(sql);
				pst.setInt(1, Integer.parseInt(userId));
				pst.setInt(2, Integer.parseInt(userId));
				ResultSet rs = pst.executeQuery();
				System.out.println("checking size of RS now...");
				if (getRSSize(rs) < 1) {
					// no results for friends
					System.out.println("No friends found");
					//return null;
				}
				populateFriendsInfo(rs, friends, userId, radii);
				System.out.println("Size of array is " + friends.size());
			} catch (SQLException e) {
				// Auto-generated catch block
				e.printStackTrace();
			}
		//}
		if (friends.size() == 0) {
			System.out.println("Size of array is zero");
			//return null;
		}
		return friends;
	}

	/**
	 * Converts a signed degrees num to radians
	 * 
	 * @param num
	 * @return
	 */
	public double toRad(double num) {
		return Math.PI * num / 180;
	}

	/**
	 * Calculates the size of the resultset. It positions the resultset at the
	 * first row after it is done;
	 * 
	 * @param rs
	 * @throws SQLException
	 */
	private int getRSSize(ResultSet rs) throws SQLException {
		rs.last();
		int size = rs.getRow();
		if (size > 0)
			rs.first();

		return size;
	}

	/**
	 * Returns an array list populated with friends that are in the distance
	 * 
	 * @param rs
	 * @param al arraylist which will be modified
	 * @param userId
	 * @param radii radius the friend must be within, in metres
	 * @return
	 * @throws SQLException
	 */
	private boolean populateFriendsInfo(ResultSet rs, ArrayList<FriendInfoDTO> al, String userId, long radii) 
		throws SQLException {
		
		if(rs.isFirst())
			rs.beforeFirst();
		
		while(rs.next()) {
			if(rs.getLong("distance") > radii) {
				System.out.println("Friend found. Distance exceeds: " + rs.getLong("distance"));
				continue; // skip the friend whose distance is larger than required
			}
			FriendInfoDTO fd = new FriendInfoDTO();
			fd.userID = Integer.parseInt(userId);
			fd.friendID = rs.getInt("friend");
			fd.distance = rs.getLong("distance");
			if(rs.wasNull())
				continue; // no distance was calculate for this pair of friends
			fd.friendEmail = rs.getString("email");
			fd.friendName = rs.getString("name");
			fd.friendPhone = rs.getString("phone_num");
			fd.dtLoc = rs.getDate("updated_on");
			al.add(fd);
			System.out.println(fd.toXML());
		}
		return true;
	}
}
